﻿/*
 * This file is part of MonoStrategy.
 *
 * Copyright (C) 2010-2011 Christoph Husse
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU Affero General Public License as
 *  published by the Free Software Foundation, either version 3 of the
 *  License, or (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU Affero General Public License for more details.
 *
 *  You should have received a copy of the GNU Affero General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * Authors: 
 *      # Christoph Husse
 * 
 * Also checkout our homepage: http://monostrategy.codeplex.com/
 */
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Drawing;
using System.Drawing.Imaging;
using System.Threading;
using System.ComponentModel;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
using MonoStrategy;

namespace MonoStrategy
{
    [Serializable]
    public enum GFXSequenceType : int
    {
        Landscape = 1,
        GUI = 2,
        Objects = 3,
        Torso = 4,
        Shadow = 5,
    }

    [Serializable]
    public class GFXSequence
    {
        private List<GFXFrame> m_Frames = new List<GFXFrame>();

        public Int32 Index { get; private set; }
        public GFXSequenceType Type { get; private set; }
        public GFXFile File { get; private set; }
        public List<GFXFrame> Frames { get { return m_Frames; } }
        public Bitmap Image
        {
            get
            {
                if (m_Frames.Count == 0)
                    return null;

                return m_Frames[0].Image;
            }
        }

        public GFXSequence(GFXFile inFile, GFXSequenceType inType, int inIndex)
        {
            if (inFile == null)
                throw new ArgumentNullException();

            File = inFile;
            Type = inType;
            Index = inIndex;
        }

        public void AddFrame(GFXFrame inFrame)
        {
            m_Frames.Add(inFrame);
        }
    }

    [Serializable]
    public class GFXFrame
    {
        public GFXSequence Sequence { get; private set; }
        public Int32 Width { get; private set; }
        public Int32 Height { get; private set; }
        public Bitmap Image { get; private set; }
        public Int32 OffsetX { get; set; }
        public Int32 OffsetY { get; set; }
        public Int32 Index { get; private set; }

        private GFXFrame()
        {
        }

        public static GFXFrame FromImage(
            System.Drawing.Image inImage,
            int inWidth,
            int inHeight,
            GFXSequence inSequence,
            int inFrameIndex)
        {
            GFXFrame result = new GFXFrame()
            {
                Width = inWidth,
                Height = inHeight,
                Sequence = inSequence,
                Index = inFrameIndex,
                Image = new Bitmap(inWidth, inHeight, PixelFormat.Format32bppArgb),
            };

            Graphics.FromImage(result.Image).DrawImageUnscaledAndClipped(inImage, new System.Drawing.Rectangle(0, 0, inWidth, inHeight));

            return result;
        }
    }


    [Serializable]
    public class GFXFile : IDisposable
    {
        private readonly List<GFXSequence> m_LandscapeSeqs = new List<GFXSequence>();
        private readonly List<GFXSequence> m_GUISeqs = new List<GFXSequence>();
        private readonly List<GFXSequence> m_ObjectSeqs = new List<GFXSequence>();
        private readonly List<GFXSequence> m_TorsoSeqs = new List<GFXSequence>();
        private readonly List<GFXSequence> m_ShadowSeqs = new List<GFXSequence>();

        [NonSerialized]
        private Thread m_LoaderThread;
        [NonSerialized]
        private Bitmap m_PixBuffer;
        [NonSerialized]
        private FileStream m_Stream;
        [NonSerialized]
        private BinaryReader m_Reader;
        [NonSerialized]
        private Object m_Lock = new Object();
        [NonSerialized]
        private Graphics m_Graphics;

        public Int32 Checksum { get; private set; }

        public List<GFXSequence> LandscapeSeqs { get { return m_LandscapeSeqs; } }
        public List<GFXSequence> GUISeqs { get { return m_GUISeqs; } }
        public List<GFXSequence> ObjectSeqs { get { return m_ObjectSeqs; } }
        public List<GFXSequence> TorsoSeqs { get { return m_TorsoSeqs; } }
        public List<GFXSequence> ShadowSeqs { get { return m_ShadowSeqs; } }

        public string FileName { get; private set; }
        public Boolean IsLoaded { get; private set; }
        public Double Progress { get; private set; }

        public void CancelLoad()
        {
            var thread = m_LoaderThread;

            if (thread != null)
                thread.Abort();
        }

        public void WaitForLoad()
        {
            var thread = m_LoaderThread;

            if (thread != null)
                thread.Join();
        }

        public GFXFile()
        {
        }

        public void Dispose()
        {
            Reset();
        }

        public void Reset()
        {
            FileName = null;
            lock (m_LandscapeSeqs) { m_LandscapeSeqs.Clear(); }
            lock (m_LandscapeSeqs) { m_GUISeqs.Clear(); }
            lock (m_LandscapeSeqs) { m_ObjectSeqs.Clear(); }
            lock (m_LandscapeSeqs) { m_TorsoSeqs.Clear(); }
            lock (m_LandscapeSeqs) { m_ShadowSeqs.Clear(); }
        }

        public void Save()
        {
            FileStream stream = File.OpenWrite(FileName + ".cache");
            BinaryFormatter format = new BinaryFormatter();

            using (stream)
            {
                format.Serialize(stream, this);
            }
        }

        public void BeginLoad(String inFilename)
        {
            Thread thread = new Thread(() =>
            {
                try
                {
                    // look for cached extraction
                    BinaryFormatter format = new BinaryFormatter();
                    FileStream stream;

                    try
                    {
                        if (File.Exists(inFilename + ".cache"))
                        {
                            GFXFile file;

                            stream = File.OpenRead(inFilename + ".cache");

                            using (stream)
                            {
                                file = (GFXFile)format.Deserialize(stream);
                            }

                            lock (m_LandscapeSeqs) { this.m_GUISeqs.AddRange(file.m_GUISeqs); }
                            lock (m_LandscapeSeqs) { this.m_LandscapeSeqs.AddRange(file.m_LandscapeSeqs); }
                            lock (m_LandscapeSeqs) { this.m_ObjectSeqs.AddRange(file.m_ObjectSeqs); }
                            lock (m_LandscapeSeqs) { this.m_ShadowSeqs.AddRange(file.m_ShadowSeqs); }
                            lock (m_LandscapeSeqs) { this.m_TorsoSeqs.AddRange(file.m_TorsoSeqs); }

                            IsLoaded = true;

                            return;
                        }
                    }
                    catch
                    {
                    }

                    // parse original file
                    InternalLoad(inFilename);

                    // cache extraction for next load
                    stream = File.OpenWrite(inFilename + ".cache");

                    using (stream)
                    {
                        format.Serialize(stream, this);
                    }
                }
                catch (ThreadAbortException)
                {
                }
                catch (Exception e)
                {
                    Log.LogExceptionModal(e);
                }
                finally
                {
                    FileName = inFilename;

                    if (!IsLoaded)
                        Reset();

                    m_LoaderThread = null;
                    Progress = 0;
                }
            });

            lock (m_Lock)
            {
                if (m_LoaderThread != null)
                    throw new InvalidOperationException("Simultaneous loads are not permitted!");

                Reset();

                IsLoaded = false;
                Progress = 0;

                m_PixBuffer = new Bitmap(1024, 1204, PixelFormat.Format32bppArgb);
                m_Graphics = Graphics.FromImage(m_PixBuffer);
                m_Stream = File.OpenRead(inFilename);
                m_Reader = new BinaryReader(m_Stream);

                m_LoaderThread = thread;
                m_LoaderThread.IsBackground = true;
                m_LoaderThread.Start();
            }
        }

        private void InternalLoad(String inFileName)
        {
            try
            {
                // compute checksum
                byte[] hash = System.Security.Cryptography.MD5.Create().ComputeHash(m_Stream);

                for (int i = 0; i < 4; i++)
                {
                    Checksum |= (((Int32)hash[i]) << (i * 8));
                }

                int fileSize = ReadDWordAt(48);

                if (fileSize != m_Stream.Length)
                    throw new InvalidDataException("File size read from file does not match actual file size.");

                // read frame counts of sequences and their addresses
                int offs = 52;
                int[] seqAddresses = new int[6];
                int[] seqCounts = new int[6];
                int seqCount = 0;

                for (int i = 0; i < 6; i++)
                {
                    int check = seqAddresses[i] = ReadDWordAndIncrement(ref offs) + 6;

                    if ((check < 0) || (check > fileSize))
                        throw new InvalidDataException("Address of \"" + check + "\" is out of bounds.");
                }

                for (int i = 0; i < 6; i++)
                {
                    int check = seqCounts[i] = ReadWordAt(seqAddresses[i]);

                    if ((check < 0) || (check > 1000))
                        throw new InvalidDataException("Sequence count of \"" + check + "\" is not sane.");

                    seqCount += check;
                }

                for (int i = 0; i < 6; i++)
                {
                    seqAddresses[i] += 2;

                }

                if (!seqCounts.Any(e => e > 0))
                    throw new InvalidDataException("File does not seem to contain anything useful.");

                // process all sequences and their frames
                Double progressStep = 1.0 / seqCount;

                for (int iType = 1; iType < 6; iType++)
                {
                    GFXSequenceType type = (GFXSequenceType)iType;

                    for (int iSeq = 0; iSeq < seqCounts[iType]; iSeq++)
                    {
                        int seqStart = ReadDWordAt(seqAddresses[iType] + iSeq * 4);
                        GFXSequence sequence = new GFXSequence(this, type, iSeq);

                        if ((type == GFXSequenceType.GUI) || (type == GFXSequenceType.Landscape))
                        {
                            // no sequence info, starts right off with frames... (interpreted as sequence with one frame)
                            GFXFrame frame = ReadFrame(seqStart, sequence, 0);

                            if (frame != null)
                                sequence.AddFrame(frame);
                        }
                        else
                        {
                            // read sequence of frames
                            int byteX;
                            int relOffsTableStart;
                            int frameCount;

                            ReadSequence(seqStart, out byteX, out frameCount, out relOffsTableStart);

                            for (int iFrame = 0; iFrame < frameCount; iFrame++)
                            {
                                int picStart = ReadDWordAt(relOffsTableStart + 4 * iFrame) + byteX;
                                GFXFrame frame = ReadFrame(picStart, sequence, iFrame);

                                if (frame != null)
                                    sequence.AddFrame(frame);
                            }
                        }

                        if (sequence.Frames.Count() > 0)
                        {
                            switch (type)
                            {
                                case GFXSequenceType.Landscape: lock (m_LandscapeSeqs) { m_LandscapeSeqs.Add(sequence); } break;
                                case GFXSequenceType.GUI: lock (m_GUISeqs) { m_GUISeqs.Add(sequence); break; }
                                case GFXSequenceType.Objects: lock (m_ObjectSeqs) { m_ObjectSeqs.Add(sequence); } break;
                                case GFXSequenceType.Torso: lock (m_TorsoSeqs) { m_TorsoSeqs.Add(sequence); } break;
                                case GFXSequenceType.Shadow: lock (m_ShadowSeqs) { m_ShadowSeqs.Add(sequence); } break;
                            }
                        }

                        Progress += progressStep;
                    }
                }

                IsLoaded = true;
            }
            finally
            {
                if (!IsLoaded)
                    Reset();
            }
        }

        private int ReadDWordAt(int inPosition)
        {
            m_Reader.BaseStream.Position = inPosition;
            return m_Reader.ReadInt32();
        }

        private int ReadDWordAndIncrement(ref int refPosition)
        {
            m_Reader.BaseStream.Position = refPosition;
            var result = m_Reader.ReadInt32();
            refPosition = (int)m_Reader.BaseStream.Position;

            return result;
        }

        private int ReadWordAt(int inPosition)
        {
            m_Reader.BaseStream.Position = inPosition;
            return m_Reader.ReadInt16();
        }

        private int ReadWordAndIncrement(ref int refPosition)
        {
            m_Reader.BaseStream.Position = refPosition;
            var result = m_Reader.ReadInt16();
            refPosition = (int)m_Reader.BaseStream.Position;

            return result;
        }

        private int ReadSignedByteAt(int inPosition)
        {
            m_Reader.BaseStream.Position = inPosition;
            return m_Reader.ReadSByte();
        }

        private int ReadSignedByteAndIncrement(ref int refPosition)
        {
            m_Reader.BaseStream.Position = refPosition;
            var result = m_Reader.ReadSByte();
            refPosition = (int)m_Reader.BaseStream.Position;

            return result;
        }

        private int ReadUnsignedByteAt(int inPosition)
        {
            m_Reader.BaseStream.Position = inPosition;
            return m_Reader.ReadByte();
        }

        private int ReadUnsignedByteAndIncrement(ref int refPosition)
        {
            m_Reader.BaseStream.Position = refPosition;
            var result = m_Reader.ReadByte();
            refPosition = (int)m_Reader.BaseStream.Position;

            return result;
        }

        private void ReadSequence(int inSeqHeader, out int outByteX, out int outFrameCount, out int outRelOffsTableStart)
        {
            int offs = inSeqHeader;

            // some magic
            int code = ReadWordAndIncrement(ref offs);
            int word = ReadWordAndIncrement(ref offs);
            int b = ReadUnsignedByteAndIncrement(ref offs);
            int word2 = ReadWordAndIncrement(ref offs);

            if ((code != 5122) || (word != 0) || (b != 8) || (word2 != 0))
                throw new InvalidDataException();

            outByteX = inSeqHeader + 4;
            outFrameCount = ReadUnsignedByteAndIncrement(ref offs);

            outRelOffsTableStart = offs;
        }

        /// <summary>
        /// Converts a color given in RGB565 into RGB888.
        /// </summary>
        private Color FromRgb565(int inValue)
        {
            int blue = (inValue & 0xF800) >> 11;
            int red = (inValue & 0x001F);
            int green = (inValue & 0x07E0) >> 5;

            return Color.FromArgb(
                255,
                (int)(blue * (255.0 / (1 << 5))),
                (int)(green * (255.0 / (1 << 6))),
                (int)(red * (255.0 / (1 << 5))));
        }

        private GFXFrame ReadFrame(int inOffset, GFXSequence inSequence, int inFrameIndex)
        {
            m_Graphics.Clear(Color.FromArgb(0, 0, 0, 0));

            int startOffs = inOffset;
            int width = ReadWordAndIncrement(ref inOffset);
            int height = ReadWordAndIncrement(ref inOffset);
            int xrel = 0, yrel = 0;
            int y = 0;
            int tokoffs;
            int x = 0;
            int iPixel;

            switch (inSequence.Type)
            {
                case GFXSequenceType.Landscape:
                    // magic
                    ReadUnsignedByteAt(inOffset);
                    ReadUnsignedByteAt(inOffset + 1);
                    ReadWordAndIncrement(ref inOffset);
                    break;
                case GFXSequenceType.Torso:
                    xrel = ReadWordAndIncrement(ref inOffset);
                    yrel = ReadWordAndIncrement(ref inOffset);
                    break;
                default:
                    xrel = ReadWordAndIncrement(ref inOffset);
                    yrel = ReadWordAndIncrement(ref inOffset);

                    ReadUnsignedByteAndIncrement(ref inOffset); // magic zero

                    if (ReadUnsignedByteAt(inOffset) == 0)
                    {
                        // Hack! Ignore first zero if following byte is not (-128).
                        if (ReadSignedByteAt(inOffset + 1) != -128)
                            ReadUnsignedByteAndIncrement(ref inOffset);
                    }
                    break;
            }

            for (int token = 0; y < height; token++)
            {
                int pixelCount = ReadUnsignedByteAndIncrement(ref inOffset);
                int dist = ReadSignedByteAndIncrement(ref inOffset); // distance from last token or left image border

                if (dist < 0)
                    tokoffs = dist + 128;
                else
                    tokoffs = dist;

                x += tokoffs;

                if (y >= height)
                    throw new InvalidDataException();

                // read pixels
                for (iPixel = 0; iPixel < pixelCount; iPixel++)
                {
                    if (x >= width)
                        throw new InvalidDataException();

                    switch (inSequence.Type)
                    {
                        case GFXSequenceType.Torso:
                            {
                                // hue of player color
                                var pix = ReadUnsignedByteAndIncrement(ref inOffset);
                                m_PixBuffer.SetPixel(x, y, Color.FromArgb(Math.Max(0, Math.Min(pix << 3, 150)), 0, 0));
                            } break;
                        case GFXSequenceType.Shadow:
                            {
                                // monochrome shadow
                                m_PixBuffer.SetPixel(x, y, Color.Black);
                            } break;
                        default:
                            {
                                // colored pixels
                                var pix = ReadWordAndIncrement(ref inOffset);
                                m_PixBuffer.SetPixel(x, y, FromRgb565(pix));
                            } break;
                    }

                    x++;
                }

                // reached end of pixel line?
                if (dist < 0)
                {
                    y++;
                    x = 0;
                }
            }

            if ((width == 0) || (height == 0))
                return null;

            GFXFrame result = GFXFrame.FromImage(m_PixBuffer, width, height, inSequence, inFrameIndex);

            result.OffsetX = xrel;
            result.OffsetY = yrel;

            return result;
        }
    }
}